#include "projectmanager.h"
#include <qstringlist.h>
#include <QDirIterator>
#include <QTextStream>
#include <QApplication>
#include <QMessageBox>
//#include "libb64/encode.h"
#include "base64.h"
#include "resources.h"
#include "../lua_parser/TokenParser.h"
//#include "../lua_parser/Token.h"
void getBase64(const unsigned char * aArray, int aLength, QString & aResult)
{
    aResult = base64_encode(aArray, aLength);
}


ProjectManager::ProjectManager(QString aLastProject)
{    
    m_saved = false;
    m_fileFilter = "";
    m_isTemporaryProject = true;
    m_projectName = "LuaProject";
    //addStandardFunctions();
    if(QFileInfo(aLastProject).exists())
        load(aLastProject);

    //fill keywords list
    m_keywords <<"and"<<"break"<<"do"<<"else"<<"elseif"<<"end"
                  <<"false" <<"for"	<<"function"<<"if"<<"in" <<"local"
                  <<"nil" <<"not" <<"or"<<"repeat"<<"return"<<"then"
                  <<"true"<<"until"<<"while";
    //load icons
    getBase64(Resources::remove_png, sizeof(Resources::remove_png), m_removePngData);
    getBase64(Resources::open_png, sizeof(Resources::open_png), m_openPngData);
    getBase64(Resources::add_file_png, sizeof(Resources::add_file_png), m_addFilePngData);
    getBase64(Resources::import_png, sizeof(Resources::import_png), m_importPngData);
    getBase64(Resources::new_png, sizeof(Resources::new_png), m_newPngData);
    getBase64(Resources::new_project_png, sizeof(Resources::new_project_png), m_newProjectPngData);
}

ProjectManager::~ProjectManager()
{
    resetFiles();    
}

void ProjectManager::addHeaderHtmlItem(QString aText, QStringList aRightIconList, QStringList aRightIconLinkList, QStringList &aSink)
{
    if(aRightIconLinkList.size() != aRightIconList.size())
    {
        qDebug("Dir icons list doesn't match links list!");
        return;
    }    
    aSink << "<p>";
    aSink << aText;
    for(int i = 0; i < aRightIconList.size(); i++)
    {
        aSink << "<a href=\"" + aRightIconLinkList[i]+"\">";
        aSink << "<img style='text-decoration:none;vertical-align:middle;float:right;' src=\"data:image/png;base64," + aRightIconList[i] + "\" width=\"20\" height=\"20\"/>";
        aSink << "</a>";
    }
    aSink << "</p>";
}

void ProjectManager::addListHtmlItem(QString aLink, QString aText, QString aIcon, QStringList & aSink)
{    
    aSink << "<li>";
    aSink << "<table style='width:100%;'>";
    aSink << "<tr>";
    aSink << "<td>";
    aSink << "<a href=\"" + aLink + "\" style='display:block;width:100%;height:100%;'>";
    if(aIcon != "")
        aSink << "<img style='text-decoration:none;vertical-align:middle;' src=\"data:image/png;base64,"  + aIcon + "\" width=\"32\" height=\"32\"/>";
    aSink << "&nbsp;";
    aSink << "&nbsp;";
    aSink << "&nbsp;";
    aSink << "<b>";
    aSink << aText;
    aSink << "</b>";
    aSink << "</a>";
    aSink << "</td>";

    //adding exclude file button
    if(aLink.startsWith("open:"))
    {
        QString path = aLink.right(aLink.length() - QString("open:").length());
        aSink << "<td width='20px'>";
        aSink << "<a href=\"excludefile:" + path + "\">";
        aSink << "<img style='text-decoration:none;vertical-align:middle;float:right;' src=\"data:image/png;base64,"
                 + m_removePngData + "\" width=\"20\"" +" height=\"20\"/>";
        aSink << "</a>";
        aSink << "</td>";
    }
    aSink << "</tr>";
    aSink << "</table>";
    aSink << "</li></a>";

}

void ProjectManager::saveAllFiles()
{
    for(int i = 0; i < m_files.size(); i++)
    {
        LuaFile * file = m_files[i];
        if(!file->isSaved())
            file->save();
    }
}

bool ProjectManager::hasUnsavedFiles()
{
    for(int i = 0; i < m_files.size(); i++)
    {
        LuaFile * file = m_files[i];
        if(!file->isSaved())
            return true;
    }
    return false;
}

void ProjectManager::resetProject()
{
    m_saved = false;
    m_isTemporaryProject = true;
    m_projectName = "LuaProject";
    m_files.clear();
    m_filesByDirs.clear();
    m_currentFile = "";
}

void ProjectManager::excludeFile(QString aFile)
{
    int count = m_files.size();
    for(int i = 0; i < count; i++)
    {
        LuaFile * file = m_files[i];
        if(file->getPath() == aFile)
        {
            m_files.remove(i);
            //emit fileExcluded(file->getPath());
            m_saved = false;
            delete file;
            if(!m_isTemporaryProject)
                save(m_projectFile);
            return;
        }
    }

}

void ProjectManager::excludeDirectory(QString aDirectory)
{
    int count = m_files.size();
    QFileInfo dir(aDirectory);
    QString dirAbsolutePath = dir.absolutePath();
    QVector<LuaFile *> toExclude;
    for(int i = 0; i < count; i++)
    {
        LuaFile * file = m_files[i];
        QFileInfo info(file->getPath());
        if(info.absoluteFilePath().startsWith(dirAbsolutePath))
            toExclude.append(file);
    }
    for(int i = 0; i < toExclude.size(); i++)
        excludeFile(toExclude[i]->getPath());
}

void ProjectManager::addContent(QStringList &aHtml)
{
    if(m_files.size() == 0)
        addEmptyProjectHtml(aHtml);
    else
        addProjectHtml(aHtml);
}


void ProjectManager::addEmptyProjectHtml(QStringList & aHtml)
{    
    aHtml << "<p><h3 align=\"center\">Welcome to Luacode<h3></p>";
    aHtml << "<ul>";
    QStringList recents = SettingsManager::Instance()->getRecentProjects();
    if(recents.size() > 0)
    {
        aHtml << "<p>Recent Projects</p>";
        for(int i = 0; i < recents.size(); i++)
        {
            aHtml << "<li><b>" << recents[i] <<"</b></li>";
        }
    }

    aHtml << "<p>Quick Start</p>";
    addListHtmlItem("openproject", "Open Project", m_openPngData, aHtml);
    addListHtmlItem("addfile", "Add Files To Project", m_addFilePngData, aHtml);
    addListHtmlItem("importdir", "Import Directory To Project", m_importPngData, aHtml);
    aHtml << "</ul>";
}

void ProjectManager::addProjectHtml(QStringList & aHtml)
{
    //associate files with their parent directories
    int count = m_files.size();
    m_filesByDirs.clear();
    int filterLength = m_fileFilter.length();
    for(int i = 0; i < count; i++)
    {
        LuaFile * file = m_files[i];
        QFileInfo info(file->getPath());
        if(filterLength > 0)
        {
            if(!info.fileName().startsWith(m_fileFilter, Qt::CaseInsensitive))
                continue;

        }
        m_filesByDirs.insertMulti(info.path(), info.filePath());
    }

    aHtml << "<table width='100%'>";
    aHtml << "<tr>";
    aHtml << "<td width='50%'  style='vertical-align:top;'>";
    aHtml << "<ul>";
    QList<QString> keys = m_filesByDirs.uniqueKeys();
    qSort(keys.begin(), keys.end());
    QStringList dirButtons;
    dirButtons.append(m_removePngData);
    dirButtons.append(m_newPngData);

    for (int i = 0; i < keys.size(); i++)
    {
        //adding header with directory path        
        QString key = keys.at(i);
        QStringList dirButtonLinks;
        dirButtonLinks.append("excludedir:" + key);
        dirButtonLinks.append("newfile:" + key);
        addHeaderHtmlItem(key, dirButtons, dirButtonLinks, aHtml);
        //html << key;
        QList<QString> values = m_filesByDirs.values(key);
        qSort(values);
        for(int t = 0; t < values.size(); t++)
        {
            //adding files            
            addListHtmlItem("open:" + values.at(t),QFileInfo(values.at(t)).fileName(), "", aHtml);
        }
    }
    aHtml << "</ul>";
    aHtml << "</td>";
    aHtml << "<td  width='50%' style='vertical-align:top;'>";
    aHtml << "<ul>";
    aHtml << "<p>Project</p>";
    addListHtmlItem("openproject", "Open Project", m_openPngData, aHtml);
    addListHtmlItem("addfile", "Add Files To Project", m_addFilePngData, aHtml);
    addListHtmlItem("importdir", "Import Directory To Project", m_importPngData, aHtml);
    if(!m_isTemporaryProject)
        addListHtmlItem("newproject", "Create New Project", m_newProjectPngData, aHtml);
    QStringList recents = SettingsManager::Instance()->getRecentProjects();
    if(recents.size() > 0)
    {
        aHtml << "<p>Recent Projects</p>";
        for(int i = 0; i < recents.size(); i++)
        {
            QFileInfo info(recents[i]);
            if(info.exists())
                addListHtmlItem("openproject:" + info.filePath(), info.fileName(), "", aHtml);
        }
    }
    aHtml << "</ul>";
    aHtml << "</td>";
    aHtml << "</tr>";
    aHtml << "</table>";
}

LuaFile * ProjectManager::getFileByPath(QString aPath)
{
    for(int i = 0; i < (int)m_files.size(); i++)
    {
        if(m_files[i]->getPath() == aPath)
            return m_files[i];
    }
    return NULL;
}


void ProjectManager::importFilesFromDirectory(QString aDirectory)
{
    //resetFiles();
    //get lua files recursively
    QHash<QString, bool> existingFiles;
    for(int i = 0; i < m_files.size(); i++)
        existingFiles[m_files[i]->getPath()] = true;
    QDirIterator dirIt(aDirectory,QDirIterator::Subdirectories);
    while (dirIt.hasNext())
    {
        dirIt.next();
        if(existingFiles.contains(dirIt.filePath()))
            continue;
        QFileInfo info(dirIt.filePath());
        if (info.isFile())
        {
            if (info.suffix() == "lua")
            {
                LuaFile * file = new LuaFile(dirIt.filePath());
                file->reloadFromFileSystem(this);
                m_files.push_back(file);
            }
        }
    }
    if(!m_isTemporaryProject)
        save(m_projectFile);
}

void ProjectManager::resetFiles()
{
    for(int i = 0; i < m_files.size(); i++)
        delete m_files[i];
    m_files.clear();
    m_filesByDirs.clear();
}




void ProjectManager::setCurrentFile(QString aFile)
{
    m_currentFile = aFile;
}

QString ProjectManager::getModifiedWord(int aOffset, int aCharacter)
{
    LuaFile * file = getFileByPath(m_currentFile);
    if(file)
        return file->getTokenParser()->getModifiedWord(aOffset, aCharacter);
    return "";
}

void ProjectManager::setOffsetForCurrentFile(int aScrollOffset)
{
    LuaFile * file = getFileByPath(m_currentFile);
    if(file)
        file->setScrollOffset(aScrollOffset);
}

void ProjectManager::setFilter(QString aFilter)
{
    m_fileFilter = aFilter;
}

void ProjectManager::addNewFile(QString aPath)
{
    //creating file
    QFile file(aPath);
    QFileInfo info(aPath);
    if(info.exists())
    {
        QMessageBox messageBox;
        messageBox.setText(aPath + " exists already!");
        messageBox.setStandardButtons(QMessageBox::Ok);
        messageBox.setDefaultButton(QMessageBox::Ok);
        messageBox.exec();
        return;
    }
    file.open(QIODevice::WriteOnly);
    file.close();
    addFile(aPath);
}

void ProjectManager::getAllFiles(QStringList &aList)
{
    for(int i = 0; i < m_files.count(); i++)
    {
        aList.append(m_files[i]->getPath());
    }
}

void ProjectManager::getAllMatchingGlobals(QString aWord, QStringList &aList, QStringList &aLinks)
{
    for(int i = 0; i < m_files.count(); i++)
    {
        m_files[i]->getAllGlobalReferences(aWord, aList, aLinks, m_files[i]->getPath());
    }
}

int ProjectManager::getSuggestions(QStringList &aList, int aOffset, int aCharacter)
{
    LuaFile * file = getFileByPath(m_currentFile);
    if(file)
    {
        //may be we are inside of comment or string, we don't want suggestions there
        bool needSuggestions = file->getTokenParser()->needSuggestions(aOffset);
        if(!needSuggestions)
            return 0;
        QString word = file->getTokenParser()->getModifiedWord(aOffset, aCharacter);
        //file->addMatchingLocals(aOffset, word, aList);
        file->getLocalCompletions(word, aOffset, aList);
        //getMatchingKeywords(word, aList);
        //getMatchingStandardFunctions(word, aList);
        if(word.length() >= 2)//TODO setting
        {
            for(int i = 0; i < m_files.count(); i++)
                //m_files[i]->addMatchingGlobals(word, aList);
                m_files[i]->getGlobalCompletions(word, aList);
        }

        return word.length() - 1;
    }
    return 0;
}


QString ProjectManager::getProjectName()
{
    return m_projectName;
}

void ProjectManager::setProjectName(QString aName)
{
    m_projectName = aName;
}

bool ProjectManager::load(QString aFile)
{
    QStringList fileData;
    QFile file(aFile);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        QMessageBox messageBox;
        messageBox.critical(0,"Error","Could not open project " + aFile + "!");
        return false;
    }
    m_projectFile = aFile;
    QTextStream in(&file);
    while (!in.atEnd())
    {
        QString line = in.readLine();
        fileData.append(line);
    }
    file.close();
    int count = fileData.size();
    bool nameSection = false;
    bool filesSection = false;
    bool currentSection = false;
    m_files.clear();
    m_filesByDirs.clear();
    for(int i = 0; i < count; i++)
    {
        QString line = fileData[i];
        if(line == "[name]")
        {
            nameSection = true;
            continue;
        }
        else if(line == "[files]")
        {
            filesSection = true;
            continue;
        }
        else if(line == "[current]")
        {
            currentSection = true;
            continue;
        }
        else if(line == "[end]")
        {
            nameSection = false;
            filesSection = false;
            currentSection = false;
            continue;
        }
        if(nameSection)
            m_projectName = line;
        else if (currentSection)
            m_currentFile = line;
        else if (filesSection)
        {
            //adding file
            addFile(line);
        }
    }
    m_saved = true;
    m_isTemporaryProject = false;    
    return true;
}

bool ProjectManager::save(QString aFile)
{
    QFile file(aFile);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        QMessageBox messageBox;
        messageBox.critical(0,"Error","Unable to save project " + aFile + "!");
        return false;
    }
    m_projectName = QFileInfo(aFile).baseName();
    QTextStream out(&file);
    out << "[name]\n";
    out << m_projectName << "\n";
    out << "[end]\n";
    out << "[current]\n";
    out << m_currentFile << "\n";
    out << "[end]\n";
    out << "[files]\n";
    int count = m_files.size();
    for(int i = 0; i < count; i++)
    {
        out << m_files[i]->getPath()<<"\n";
    }
    out << "[end]\n";
    file.close();
    m_saved = true;
    m_isTemporaryProject = false;
    m_projectFile = aFile;
    return true;
}

QString ProjectManager::getProjectFile()
{
    return m_projectFile;
}

void ProjectManager::addFile(QString aPath)
{
    QFileInfo info(aPath);
    if(info.exists())
    {
        LuaFile * file = new LuaFile(info.filePath());
        file->reloadFromFileSystem(this);
        m_files.push_back(file);
    }
    if(!m_isTemporaryProject)
        save(m_projectFile);
}

bool ProjectManager::isSaved()
{
    return m_saved;
}

bool ProjectManager::hasFiles()
{
    return m_files.size() > 0;
}

bool ProjectManager::isTemporaryProject()
{
    return m_isTemporaryProject;
}




bool ProjectManager::getWordBoundaries(int aOffset, int &aStart, int &aLength)
{
    LuaFile * file = getFileByPath(m_currentFile);
    if(!file)
        return false;
    return file->getTokenParser()->getWordBoundaries(aOffset, aStart, aLength);
}

bool ProjectManager::hasGlobal(const QString & aText)
{
    int count = m_files.size();
    for(int i = 0; i < count; i++)
    {
        LuaFile * file = m_files[i];
        if(file->hasGlobal(aText))
            return true;
    }
    return false;
}

bool ProjectManager::isStandardFunction(const QString &aText)
{
    return false;//m_standardFunctions.contains(aText);
}

/*void ProjectManager::addStandardFunctions()
{
    const char * functions[] = {"assert", "collectgarbage", "dofile", "error", "getfenv", "getmetatable", "ipairs",
                                "load", "loadfile", "loadstring", "next", "pairs", "pcall", "print", "rawequal",
                                "rawget", "rawset", "select", "setfenv", "setmetatable", "tonumber",
                                "tostring", "type", "unpack", "xpcall", "coroutine.create", "coroutine.resume",
                                "coroutine.running", "coroutine.status", "coroutine.wrap", "coroutine.yield", "module",
                                "package.cpath", "package.loaded", "package.loaders", "package.loadlib", "package.path", "package.preload",
                                "package.seeall", "require", "string.byte", "string.char", "string.dump", "string.find", "string.format",
                                "string.gmatch", "string.gsub", "string.len", "string.lower", "string.match", "string.rep", "string.reverse",
                                "string.sub", "string.upper", "table.concat", "table.insert", "table.maxn", "table.remove", "table.sort", "math.abs",
                                "math.acos", "math.asin", "math.atan", "math.atan2", "math.ceil", "math.cos", "math.cosh", "math.deg", "math.exp",
                                "math.floor", "math.fmod", "math.frexp", "math.huge", "math.ldexp", "math.log", "math.log10", "math.max", "math.min",
                                "math.modf", "math.pi", "math.pow", "math.rad", "math.random", "math.randomseed", "math.sin", "math.sinh",
                                "math.sqrt", "math.tan", "math.tanh", "file.close", "file.flush", "file.lines", "file.read", "file.seek",
                                "file.setvbuf", "file.write", "io.close", "io.flush", "io.input", "io.lines", "io.open", "io.output", "io.popen", "io.read",
                                "io.tmpfile", "io.type", "io.write", "os.clock", "os.date", "os.difftime", "os.execute", "os.exit", "os.getenv", "os.remove",
                                "os.rename", "os.setlocale", "os.time", "os.tmpname", "debug.debug", "debug.getfenv", "debug.gethook", "debug.getinfo",
                                "debug.getlocal", "debug.getmetatable", "debug.getregistry", "debug.getupvalue", "debug.setfenv", "debug.sethook",
                                "debug.setlocal", "debug.setmetatable", "debug.setupvalue", "debug.traceback",
                                "gcinfo",
                                   //global variables
                                "_VERSION", "package.path", "_G",
                                //TODO add custom APIs
                                "loaddir", "load_file", "randomize", "get_crc32", "random",
                                "send", "decode_room", "disconnect_client", "send_prompt",
                                "regex_match", "encode_room", "is_solid_voxel",
                                "dig_voxel", "update_sources", "file_exists", "wrap_text",
                                "get_world_size", "voxel_value", "get_uuid",
                                "number_to_words", "gettimeofday", "soundex_compare",
                                "get_map", "save_to_file", "delete_file", "is_cavern",
                                0};
    for(int i = 0; functions[i]; i++)
    {
        QString func(functions[i]);
        AstNode node = AstNode(func, EAstStandardFunction, 0);
        m_standardFunctions[func] = node;
    }
}


void ProjectManager::getMatchingStandardFunctions(QString aMatch, QStringList &aList)
{
    QString match = aMatch.toLower();
    QList<QString> nodes = m_standardFunctions.keys();
    size_t count = nodes.size();
    int matchLength = match.length();
    QString key;
    for(size_t i = 0; i < count; i++)
    {
        key = nodes[i];
        if(LuaScope::matches(match, nodes[i], matchLength))
        {
            AstNode node = m_standardFunctions[key];
            aList.append(node.getFunctionHeader());
        }

    }
}

void ProjectManager::getMatchingKeywords(QString aMatch, QStringList &aList)
{
    QString match = aMatch.toLower();
    size_t count = m_keywords.size();
    int matchLength = match.length();
    for(size_t i = 0; i < count; i++)
    {
        if(LuaScope::matches(match, m_keywords[i], matchLength))
        {
            aList.append(m_keywords[i]);
        }

    }
}
*/

bool ProjectManager::getClickedLinkWithText(int aOffset, QString aWord, QStringList & aLinks)
{
    LuaFile * file = getFileByPath(m_currentFile);
    //search locals first
    int localReference = file->getLocalReference(aWord, aOffset);
    if(localReference != REFERENCE_NOT_FOUND)
    {
        aLinks.append("local:" + QString::number(localReference));
        return true;
    }
    //trying globals then
    //checking current file first
    int globalReference = file->getGlobalReference(aWord);
    if(globalReference != REFERENCE_NOT_FOUND)
    {
        aLinks.append("local:" + QString::number(globalReference));
        return true;
    }
    //then all others
    for(int i = 0; i < m_files.size(); i++)
    {
        LuaFile * currentFile = m_files[i];
        if(currentFile == file)
            continue;
        int globalReference = currentFile->getGlobalReference(aWord);
        if(globalReference != REFERENCE_NOT_FOUND)
        {
            aLinks.append("global:" + QString::number(globalReference)+ ":" + currentFile->getPath() +
                          ":" + aWord);
            return true;
        }
    }
    return false;
}

bool ProjectManager::getClickedLink(int aOffset, QStringList & aLinks)
{
    LuaFile * file = getFileByPath(m_currentFile);
    if(!file)
        return false;
    LuaToken *token = file->getTokenParser()->getTokenByOffset(aOffset);
    int wordOffset = token->getStart();
    if(token->getType() != ETokenWord)
        return false;
    QString word = token->getText();
    bool result = getClickedLinkWithText(wordOffset, word, aLinks);
    if(result)
        return true;
    int dotIndex = word.indexOf('.');
    if(dotIndex > 0)
    {
        //try with before-the-dot part
        QString prefix = word.left(dotIndex);
        return getClickedLinkWithText(wordOffset, prefix, aLinks);
    }

    return false;
}

/*






bool globalMatches(QString aPattern, QString aGlobal)
{
    if(aGlobal.startsWith(aPattern, Qt::CaseInsensitive))
        return true;
    QStringList components = aGlobal.split(".");
    for(int i = 0; i < components.count(); i++)
    {
        if(components[i].startsWith(aPattern, Qt::CaseInsensitive))
            return true;
    }
    components = aGlobal.split("_");
    for(int i = 0; i < components.count(); i++)
    {
        if(components[i].startsWith(aPattern, Qt::CaseInsensitive))
            return true;
    }
    return false;
}


void ProjectManager::getAllMatchingGlobals(QString aWord, QStringList & aList, QStringList &aLinks)
{
    aList.clear();
    aLinks.clear();
    QList<AstNode> list;
    for(int i = 0; i < m_files.count(); i++)
    {
        list.clear();
        m_files[i]->getAllGlobals(list);
        for(int t = 0; t < list.count(); t++)
        {
            QString header;
            if(list[t].getType() == EAstGlobalFunction)
                header = list[t].getFunctionHeader();
            else
                header = list[t].getText();
            if(globalMatches(aWord, header))
            {
                aList.append(header);
                QString link = "global:" + QString::number(list[t].getOffset())
                        + ":" + m_files[i]->getPath() + ":" + header;
                aLinks.append(link);
            }
        }
    }
}*/

bool ProjectManager::hasGlobalMethod(const QString aText)
{
    int count = m_files.size();
    for(int i = 0; i < count; i++)
    {
        LuaFile * file = m_files[i];
        if(file->hasGlobalMethod(aText))
            return true;
    }
    return false;
}


bool ProjectManager::hasStandardMethod(const QString aText)
{
    /*QList<QString> keys = m_standardFunctions.keys();
    QString key;
    for(int i = 0; i < keys.size(); i++)
    {
        key = keys[i];
        if(aText == key)
            return true;
        if(key.indexOf(".") >= 0)
        {
            QList<QString> parts = key.split(".");
            if(parts.last() == aText)
                return true;
        }
    }*/
    return false;
}


